/**
 *MazePane.java
 *@anthor Daniel Liu
 *2006-5-11 ����10:41:03
 **/
package org.lex.swing.widgets;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

import javax.swing.JPanel;

import org.lex.swing.maze.DefaultMazeModel;
import org.lex.swing.maze.MazeModel;


public class MazePane extends JPanel {
	private int rowCount = 50;
	private int columnCount = 50;
	private MazeModel mazeModel = null;

	private Color[] colors = new Color[7];
	private BufferedImage image = null;
	private Graphics2D g2d = null;

	private int initialDelay = 500;
	private int delay = 10;

	private Dimension size = null;
	private double cellWidth = 0;
	private double cellHeight = 0;

	public MazePane() {
		this(50, 50);
	}

	public MazePane(int count) {
		this(count, count);
	}

	public MazePane(int row, int column) {
		super();
		this.rowCount = row;
		this.columnCount = column;
		initialize();
		loop();
	}

	private void initialize() {
		if (0 == rowCount % 2)
			rowCount++;
		if (0 == columnCount % 2)
			columnCount++;
		colors[MazeModel.WALL] = Color.gray;
		colors[MazeModel.PATH] = new Color(0, 128, 0);
		colors[MazeModel.CORRIDORS] = Color.black;
		colors[MazeModel.VISITED] = new Color(0, 0, 128);
		colors[MazeModel.ENTRY] = Color.green;
		colors[MazeModel.EXIT] = Color.red;
		colors[MazeModel.BACKGROUND] = Color.black;
		this.size = this.getSize();
		this.setBackground(colors[MazeModel.BACKGROUND]);

	}

	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		this.checkIfCanvasSizeChanged();
		g.drawImage(this.image, 0, 0, this);
	}

	private void checkIfCanvasSizeChanged() {
		int width = this.getWidth();
		int height = this.getHeight();
		if (width != size.width || height != size.height) {
			this.cellHeight = (double) height / this.rowCount;
			this.cellWidth = (double) width / this.columnCount;
			this.size.setSize(width, height);
			this.image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
			this.g2d = this.image.createGraphics();
			this.drawMazeToImage(g2d, mazeModel);
		}
	}

	private void loop() {
		new Thread(new Runnable() {
			public void run() {
				try {
					mazeModel = new DefaultMazeModel(rowCount, columnCount);
					while (true) {
						Thread.sleep(initialDelay);
						drawMazeToImage(g2d, mazeModel);
						recursiveSolveMaze(mazeModel, mazeModel.getEntry(), mazeModel.getExit());
						mazeModel.shuffle();
						repaint();
					}
				} catch (InterruptedException e) {
				}
			}
		}).start();
	}

	private boolean recursiveSolveMaze(MazeModel mazeModel, Point entry, Point exit) {
		this.repaint();
		int row = entry.x;
		int col = entry.y;
		if (mazeModel.get(row, col) == MazeModel.CORRIDORS) {
			mazeModel.set(row, col, MazeModel.PATH); // add this cell to the
			// PATH
			fillCell(g2d, row, col, MazeModel.PATH);
			if (row == exit.x && col == exit.y)
				return true; // PATH has reached goal
			try {
				Thread.sleep(delay);
				// try to solve mazeModel by extending PATH in each possible
				// direction
				if (true == recursiveSolveMaze(mazeModel, new Point(entry.x - 1, entry.y), exit)
						|| recursiveSolveMaze(mazeModel, new Point(entry.x, entry.y - 1), exit)
						|| recursiveSolveMaze(mazeModel, new Point(entry.x + 1, entry.y), exit)
						|| recursiveSolveMaze(mazeModel, new Point(entry.x, entry.y + 1), exit))
					return true;
				// mazeModel can't be solved from this cell, so backtract out of
				// the cell
				// mark cell as having been visited
				mazeModel.set(row, col, MazeModel.VISITED);
				fillCell(g2d, row, col, MazeModel.VISITED);
				Thread.sleep(delay);
			} catch (InterruptedException e) {
			}
		}
		entry = mazeModel.getEntry();
		this.fillCell(g2d, entry.x, entry.y, MazeModel.ENTRY);
		return false;
	}

	private void fillCell(Graphics2D g, int row, int col, int color) {
		g.setColor(colors[color]);
		this.fillRectangle2D(g, col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
	}

	private void drawMazeToImage(Graphics2D g, MazeModel mazeModel) {
		if (null == mazeModel)
			return;
		g.setColor(colors[MazeModel.BACKGROUND]);
		g.fillRect(0, 0, size.width, size.height);

		double x = 0, y = 0, cx = 0, cy = 0;
		for (int row = 0; row < rowCount; row++)
			for (int col = 0; col < columnCount; col++) {
				x = col * cellWidth;
				y = row * cellHeight;
				g.setColor(colors[mazeModel.get(row, col)]);
				if (mazeModel.get(row, col) == MazeModel.WALL) {
					cx = x + cellWidth / 2;
					cy = y + cellHeight / 2;
					// top
					if (0 != row && MazeModel.WALL == mazeModel.get(row - 1, col))
						this.drawLine2D(g, cx, cy, cx, cy - cellHeight / 2);
					// left
					if (0 != col && MazeModel.WALL == mazeModel.get(row, col - 1))
						this.drawLine2D(g, cx, cy, cx - cellWidth / 2, cy);
					// bottom
					if (rowCount - 1 != row && MazeModel.WALL == mazeModel.get(row + 1, col))
						this.drawLine2D(g, cx, cy, cx, cy + cellHeight / 2);
					// right
					if (columnCount - 1 != col && MazeModel.WALL == mazeModel.get(row, col + 1))
						this.drawLine2D(g, cx, cy, cx + cellWidth / 2, cy);
				} else
					this.fillRectangle2D(g, x, y, cellWidth, cellHeight);
			}
		// draw entry & exit
		Point cell = mazeModel.getEntry();
		this.fillCell(g, cell.x, cell.y, MazeModel.ENTRY);
		cell = mazeModel.getExit();
		this.fillCell(g, cell.x, cell.y, MazeModel.EXIT);
	}

	private Line2D.Double line = new Line2D.Double();

	private void drawLine2D(Graphics2D g, double x1, double y1, double x2, double y2) {
		line.setLine(x1, y1, x2, y2);
		g.draw(line);
	}

	private Rectangle2D.Double rect = new Rectangle2D.Double();

	private void fillRectangle2D(Graphics2D g, double x, double y, double w, double h) {
		rect.setRect(x, y, w, h);
		g.fill(rect);
	}
}
